Passed
Pull Request — master (#136)
by
unknown
01:53
created

index.ts ➔ write   A

Complexity

Conditions 4

Size

Total Lines 5
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 5
c 0
b 0
f 0
rs 9.7
cc 4
1
import * as fs from 'fs'
2
import * as ID3Util from './src/ID3Util'
3
import * as ID3Helpers from './src/ID3Helpers'
4
import { isFunction, isString } from './src/util'
5
import { Tags, WriteTags } from './src/types/Tags'
6
import { Options } from './src/types/Options'
7
import { updateTags } from './src/update'
8
9
export { Tags, WriteTags } from "./src/types/Tags"
10
export { Options } from "./src/types/Options"
11
export { TagConstants } from './src/definitions/TagConstants'
12
13
// Used specification: http://id3.org/id3v2.3.0
14
15
export type WriteCallback = {
16
    (error: null, data: Buffer): void
17
    (error: NodeJS.ErrnoException | Error, data: null): void
18
}
19
20
export type ReadCallback = {
21
    (error: NodeJS.ErrnoException | Error, tags: null): void
22
    (error: null, tags: Tags): void
23
}
24
25
export type RemoveCallback =
26
    (error: NodeJS.ErrnoException | Error | null) => void
27
28
export type CreateCallback =
29
    (data: Buffer) => void
30
31
/**
32
 * Remove already written ID3-Frames from a buffer
33
 */
34
export function removeTagsFromBuffer(data: Buffer) {
35
    const framePosition = ID3Util.getFramePosition(data)
36
37
    if (framePosition === -1) {
38
        return data
39
    }
40
41
    const encodedSize = data.subarray(framePosition + 6, framePosition + 10)
42
    if (!ID3Util.isValidEncodedSize(encodedSize)) {
43
        return false
44
    }
45
46
    if (data.length >= framePosition + 10) {
47
        const size = ID3Util.decodeSize(encodedSize)
48
        return Buffer.concat([
49
            data.subarray(0, framePosition),
50
            data.subarray(framePosition + size + 10)
51
        ])
52
    }
53
54
    return data
55
}
56
57
function writeInBuffer(tags: Buffer, buffer: Buffer) {
58
    buffer = removeTagsFromBuffer(buffer) || buffer
59
    return Buffer.concat([tags, buffer])
60
}
61
62
function writeAsync(tags: Buffer, filepath: string, callback: WriteCallback) {
63
    fs.readFile(filepath, (error, data) => {
64
        if(error) {
65
            callback(error, null)
66
            return
67
        }
68
        const newData = writeInBuffer(tags, data)
69
        fs.writeFile(filepath, newData, 'binary', (error) => {
70
            if (error) {
71
                callback(error, null)
72
            } else {
73
                callback(null, newData)
74
            }
75
        })
76
    })
77
}
78
79
function writeSync(tags: Buffer, filepath: string) {
80
    try {
81
        const data = fs.readFileSync(filepath)
82
        const newData = writeInBuffer(tags, data)
83
        fs.writeFileSync(filepath, newData, 'binary')
84
        return true
85
    } catch(error) {
86
        return error as Error
87
    }
88
}
89
90
/**
91
 * Write passed tags to a file/buffer
92
 */
93
export function write(tags: WriteTags, buffer: Buffer): Buffer
94
export function write(tags: WriteTags, filepath: string): true | Error
95
export function write(
96
    tags: WriteTags, filebuffer: string | Buffer, callback: WriteCallback
97
): void
98
export function write(
99
    tags: WriteTags,
100
    filebuffer: string | Buffer,
101
    callback?: WriteCallback
102
): Buffer | true | Error | void {
103
    const tagsBuffer = create(tags)
104
105
    if(isFunction(callback)) {
106
        if (isString(filebuffer)) {
107
            return writeAsync(tagsBuffer, filebuffer, callback)
108
        }
109
        return callback(null, writeInBuffer(tagsBuffer, filebuffer))
110
    }
111
    if(isString(filebuffer)) {
112
        return writeSync(tagsBuffer, filebuffer)
113
    }
114
    return writeInBuffer(tagsBuffer, filebuffer)
115
}
116
117
/**
118
 * Creates a buffer containing the ID3 Tag
119
 */
120
export function create(tags: WriteTags): Buffer
121
export function create(tags: WriteTags, callback: CreateCallback): void
122
export function create(tags: WriteTags, callback?: CreateCallback) {
123
    const frames = ID3Helpers.createBufferFromTags(tags)
124
125
    //  Create ID3 header
126
    const header = Buffer.alloc(10)
127
    header.fill(0)
128
    header.write("ID3", 0)              //File identifier
129
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
130
    header.writeUInt16BE(0x0000, 5)     //Flags 00
131
    ID3Util.encodeSize(frames.length).copy(header, 6)
132
133
    const id3Data = Buffer.concat([header, frames])
134
135
    if(isFunction(callback)) {
136
        return callback(id3Data)
137
    }
138
    return id3Data
139
}
140
141
function readSync(filebuffer: string | Buffer, options: Options): Tags {
142
    if(isString(filebuffer)) {
143
        filebuffer = fs.readFileSync(filebuffer)
144
    }
145
    return ID3Helpers.getTagsFromBuffer(filebuffer, options)
146
}
147
148
function readAsync(
149
    filebuffer: string | Buffer,
150
    options: Options,
151
    callback: ReadCallback
152
) {
153
    if(isString(filebuffer)) {
154
        fs.readFile(filebuffer, (error, data) => {
155
            if(error) {
156
                callback(error, null)
157
            } else {
158
                callback(null, ID3Helpers.getTagsFromBuffer(data, options))
159
            }
160
        })
161
    } else {
162
        callback(null, ID3Helpers.getTagsFromBuffer(filebuffer, options))
163
    }
164
}
165
166
/**
167
 * Read ID3-Tags from passed buffer/filepath
168
 */
169
export function read(filebuffer: string | Buffer, options?: Options): Tags
170
export function read(filebuffer: string | Buffer, callback: ReadCallback): void
171
export function read(
172
    filebuffer: string | Buffer, options: Options, callback: ReadCallback
173
): void
174
export function read(
175
    filebuffer: string | Buffer,
176
    optionsOrCallback?: Options | ReadCallback,
177
    callback?: ReadCallback
178
): Tags | void {
179
    const options: Options =
180
        (isFunction(optionsOrCallback) ? {} : optionsOrCallback) ?? {}
181
    callback =
182
        isFunction(optionsOrCallback) ? optionsOrCallback : callback
183
184
    if(isFunction(callback)) {
185
        return readAsync(filebuffer, options, callback)
186
    }
187
    return readSync(filebuffer, options)
188
}
189
190
/**
191
 * Update ID3-Tags from passed buffer/filepath
192
 */
193
export function update(
194
    tags: WriteTags,
195
    buffer: Buffer,
196
    options?: Options
197
): Buffer
198
export function update(
199
    tags: WriteTags,
200
    filepath: string,
201
    options?: Options
202
): true | Error
203
export function update(
204
    tags: WriteTags,
205
    filebuffer: string | Buffer,
206
    callback: WriteCallback
207
): void
208
export function update(
209
    tags: WriteTags,
210
    filebuffer: string | Buffer,
211
    options: Options,
212
    callback: WriteCallback
213
): void
214
export function update(
215
    tags: WriteTags,
216
    filebuffer: string | Buffer,
217
    optionsOrCallback?: Options | WriteCallback,
218
    callback?: WriteCallback
219
): Buffer | true | Error | void {
220
    const options: Options =
221
        (isFunction(optionsOrCallback) ? {} : optionsOrCallback) ?? {}
222
    callback =
223
        isFunction(optionsOrCallback) ? optionsOrCallback : callback
224
225
    const currentTags = read(filebuffer, options)
226
    const updatedTags = updateTags(tags, currentTags)
227
    if (isFunction(callback)) {
228
        return write(updatedTags, filebuffer, callback)
229
    }
230
    if (isString(filebuffer)) {
231
        return write(updatedTags, filebuffer)
232
    }
233
    return write(updatedTags, filebuffer)
234
}
235
236
function removeTagsSync(filepath: string) {
237
    let data
238
    try {
239
        data = fs.readFileSync(filepath)
240
    } catch(error) {
241
        return error as Error
242
    }
243
244
    const newData = removeTagsFromBuffer(data)
245
    if(!newData) {
246
        return false
247
    }
248
249
    try {
250
        fs.writeFileSync(filepath, newData, 'binary')
251
    } catch(error) {
252
        return error as Error
253
    }
254
255
    return true
256
}
257
258
function removeTagsAsync(filepath: string, callback: RemoveCallback) {
259
    fs.readFile(filepath, (error, data) => {
260
        if(error) {
261
            callback(error)
262
            return
263
        }
264
265
        const newData = removeTagsFromBuffer(data)
266
        if(!newData) {
267
            callback(error)
268
            return
269
        }
270
271
        fs.writeFile(filepath, newData, 'binary', (error) => {
272
            if(error) {
273
                callback(error)
274
            } else {
275
                callback(null)
276
            }
277
        })
278
    })
279
}
280
281
/**
282
 * Remove already written ID3-Frames from a file
283
 */
284
export function removeTags(filepath: string): boolean | Error
285
export function removeTags(filepath: string, callback: RemoveCallback): void
286
export function removeTags(filepath: string, callback?: RemoveCallback) {
287
    if(isFunction(callback)) {
288
        return removeTagsAsync(filepath, callback)
289
    }
290
    return removeTagsSync(filepath)
291
}
292
293
type Settle<T> = {
294
    (error: NodeJS.ErrnoException | Error, result: null): void
295
    (error: null, result: T): void
296
}
297
298
function makePromise<T>(callback: (settle: Settle<T>) => void) {
299
    return new Promise<T>((resolve, reject) => {
300
        callback((error, result) => {
301
            if(error) {
302
                reject(error)
303
            } else {
304
                // result can't be null here according the Settle callable
305
                // type but TS can't evaluate it properly here, so use the
306
                // null assertion, and then disable the lint error.
307
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308
                resolve(result!)
309
            }
310
        })
311
    })
312
}
313
314
export const Promises = {
315
    create: (tags: Tags) =>
316
        makePromise((settle: Settle<Buffer>) =>
317
            create(tags, result => settle(null, result)),
318
    ),
319
    write: (tags: Tags, filebuffer: string | Buffer) =>
320
        makePromise<Buffer>((callback: WriteCallback) =>
321
            write(tags, filebuffer, callback)
322
        ),
323
    update: (tags: Tags, filebuffer: string | Buffer, options?: Options) =>
324
        makePromise<Buffer>((callback: WriteCallback) =>
325
            update(tags, filebuffer, options ?? {}, callback)
326
        ),
327
    read: (file: string, options?: Options) =>
328
        makePromise((callback: ReadCallback) =>
329
            read(file, options ?? {}, callback)
330
        ),
331
    removeTags: (filepath: string) =>
332
        makePromise((settle: Settle<void>) =>
333
            removeTags(
334
                filepath,
335
                (error) => error ? settle(error, null) : settle(null)
336
            )
337
        )
338
} as const
339
340
/**
341
 * @deprecated consider using `Promises` instead, `Promise` creates conflict
342
 *             with the Javascript native promise.
343
 */
344
export { Promises as Promise }
345